#!/usr/bin/perl

######################################################################
package Swignition::uF::hCard;
######################################################################

use CGI::Util;
use Swignition::Export;
use Swignition::Export::Contact;
use Swignition::GenericParser::Utils;
use Swignition::MagicString;
use Swignition::uF;
use XML::LibXML qw(:all);
use strict;

sub cc
{
	$_ = shift;
	s/\-([a-z])/uc($1)/eg;
	return $_;
}

sub consume
{
	my $page        = shift;
	my $pageURL     = $page->uri;
	my $pageSubject = $page->{RDF}->{$pageURL};

	unless ( $page->{Profiles}->{'http://www.w3.org/2006/03/hcard'}
	||       $page->{Profiles}->{'http://purl.org/uF/hCard/1.0/'}
	||       $page->{Profiles}->{'http://purl.org/uF/2008/03/'}
	||      !$page->{options}->{uf_strict_profiles})
	{
		return;
	}
	
	my %levels;
	my $with_rel_me;
	my $with_url_source;
	
	$page->{uF}->{hCard} = [ Swignition::uF::hCard::parse_all($page) ];
	foreach my $hcard (@{ $page->{uF}->{hCard} })
	{
		$page->mergeSubject($hcard->rdf_subject);
		$hcard->rdf_validate($page);
		
		my @xpath_parts = split /\//, $hcard->{_dom}->getAttribute('_xpath');
		for (my $i=0; defined $xpath_parts[$i]; $i++) 
		{
			if ($xpath_parts[$i] =~ /^address/i)
			{
				$levels{$i} = $hcard
					unless (defined $levels{$i});
			}
		}
		
		if ($hcard->{'_has_relme'})
		{
			$with_rel_me = $hcard unless ($with_rel_me);
		}
		
		foreach my $u (@{$hcard->{url}})
		{
			if ($u eq $pageURL)
			{
				$with_url_source = $hcard unless ($with_url_source);
			}
		}
		
		foreach my $c (@{$hcard->{tel}})
			{ $page->mergeSubject($c->rdf_subject); }
		foreach my $c (@{$hcard->{email}})
			{ $page->mergeSubject($c->rdf_subject); }
		foreach my $c (@{$hcard->{org}})
			{ $page->mergeSubject($c->rdf_subject); }
		foreach my $c (@{$hcard->{impp}})
			{ $page->mergeSubject($c->rdf_subject); }
		foreach my $c (@{$hcard->{label}})
			{ $page->mergeSubject($c->rdf_subject); }
		foreach my $c (@{$hcard->{n}})
			{ $page->mergeSubject($c->rdf_subject); }
	}
	if (defined $page->{uF}->{hCard}->[0])
		{ $page->mergeNS( $page->{uF}->{hCard}->[0] ); }
	
	# Determine "shallowest" <address> element: take this to be the contact
	# for the page.
	if (keys %levels)
	{
		my @sorted_levels = sort keys %levels;
		my $shallow_level = shift @sorted_levels;
		my $contact_hcard = $levels{$shallow_level};
		$pageSubject->addVal('http://purl.org/uF/hCard/terms/contact',
			Swignition::RDFModel::Value->new($contact_hcard->uri, 1));
	}
	
	unless (firstOf($pageSubject, 'http://purl.org/uF/hCard/terms/representative'))
	{		
		# Representative hCard
		my @topics = allOf($pageSubject, 'http://xmlns.com/foaf/0.1/primaryTopic');
		my $rep;
		if (@topics)
		{
			foreach my $topic (@topics)
			{
				my $topicSubject = findSubject($page->{RDF}, $topic);
				$rep = $topicSubject
					if ($topicSubject && Swignition::Export::Contact::is($topicSubject));
			}
		}
		$rep = $rep || $with_rel_me || $with_url_source || $page->{uF}->{hCard}->[0];
		if ($rep)
		{
			$pageSubject->addVal('http://purl.org/uF/hCard/terms/representative',
				Swignition::RDFModel::Value->new($rep->uri, 1));
		}
	}
	
}


sub parse_all
{
	my $page    = shift;
	my $within  = shift || $page->{DOM};
	my $isInCal = shift || 0;
	my @nodes   = searchClass('vcard', $within);
	my @rv;
	
	foreach my $card (@nodes)
	{
		my $hcard = parse($page, $card, $isInCal);
		push @rv, $hcard;
	}
	
	return @rv;
} #/sub parse_all


sub uri
{
	my $this = shift;
	my $all  = shift;

	my @rv;
	@rv = @{$this->{IDENTIFIERS}}
		if ($this->{IDENTIFIERS});
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_xpath}, 'Contact')
		unless (@rv);

	unless (defined $all) { return $rv[0];}
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri


sub rdf_subject
{
	my $this    = shift;
	my $ns      = 'http://www.w3.org/2006/vcard/ns#';

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new($ns.'VCard', 1));
	
	# Patterns for simple data.
	my @_SINGLES  = qw(fn bday tz sort-string class dday gender kind uid);
	my @_SINGLESR = qw();
	my @_MULTIS   = qw(nickname title role key mailer note rev lang);
	my @_MULTISR  = qw(url photo sound logo caladruri source);
	my @_OBJECTS  = qw(category adr geo agent tel email org impp n label species caluri fburl);
	my @_1OBJECTS = qw(birth death);
	
	foreach my $linktype (keys %{$this->{SGNURI}})
	{
		my $predicate = 'http://buzzword.org.uk/rdf/personal-link-types#'.$linktype;
		$predicate = 'http://xmlns.com/foaf/0.1/openid' if ($linktype eq 'openid');
		$predicate = 'http://xmlns.com/foaf/0.1/weblog' if ($linktype eq 'blog');
		$predicate = 'http://xmlns.com/foaf/0.1/depiction' if ($linktype eq 'photo');
		
		$subject->addVal($predicate,
			Swignition::RDFModel::Value->new($this->{SGNURI}->{$linktype}, 1))
			if ($this->{SGNURI}->{$linktype});
	}
	
	if ($this->{isInCal})
		{ push @_SINGLES, qw(cutype rsvp partstat member delegated-from sent-by); }
	else
		{ push @_OBJECTS, qw(member); }
	
	# Properties which exist as singles.
	foreach my $property (@_SINGLES)
	{
		next unless (defined $this->{$property});
		my $val = Swignition::RDFModel::Value->new($this->{$property});

		if ($property =~ /^(cutype|rsvp|partstat|member|delegated.from|sent.by)$/)
			{ $subject->addVal('http://buzzword.org.uk/rdf/icalx#'.cc($property), $val); }
		elsif ($property =~ /^(gender|kind|dday)$/)
			{ $subject->addVal('http://buzzword.org.uk/rdf/vcardx#'.$property, $val); }
		else
			{ $subject->addVal($ns.$property, $val); }
	}

	# Resource-type properties which exist as singles.
	foreach my $property (@_SINGLESR)
	{
		next unless (length $this->{$property});
		my $val = Swignition::RDFModel::Value->new($this->{$property}, 1);
		$subject->addVal($ns.$property, $val);
	}
		
	# Properties which exist as multiple values.
	foreach my $property (@_MULTIS)
	{
		foreach my $V (@{ $this->{$property} })
		{
			my $myns = $ns;
			if ($property =~ /^(lang)$/)
				{ $myns = 'http://buzzword.org.uk/rdf/vcardx#'; }
			next unless (length $V);
			my $val = Swignition::RDFModel::Value->new($V);
			$subject->addVal($myns.$property, $val);
		}
	}
	
	# Resource-type properties which exist as multiple values.
	foreach my $property (@_MULTISR)
	{
		foreach my $V (@{ $this->{$property} })
		{
			my $myns = $ns;
			if ($property =~ /^(caladruri|source)$/)
				{ $myns = 'http://buzzword.org.uk/rdf/vcardx#'; }
			next unless (length $V);
			my $val = Swignition::RDFModel::Value->new($V, 1);
			$subject->addVal($myns.$property, $val);
		}
	}

	# Embedded objects which may be represented as resources
	foreach my $property (@_OBJECTS)
	{
		foreach my $V (@{ $this->{$property} })
		{
			my $val;			
			if (UNIVERSAL::can($V, 'uri'))
				{ $val = Swignition::RDFModel::Value->new($V->uri, 1); }
			elsif ($property =~ /^(member|caluri|fburl)$/)
				{ $val = Swignition::RDFModel::Value->new($V, 1); }
			else
				{ $val = Swignition::RDFModel::Value->new($V); }
				
			if ($property eq 'species')
				{ $subject->addVal('http://purl.org/NET/biol/ns#hasTaxonomy', $val); }
			elsif ($property =~ /^(impp|caluri|fburl|member)$/)
				{ $subject->addVal('http://buzzword.org.uk/rdf/vcardx#'.$property, $val); }
			else
				{ $subject->addVal($ns.$property, $val); }
		}
	}

	# Embedded objects which may be represented as resources
	foreach my $property (@_1OBJECTS)
	{
		if ( $this->{$property} )
		{
			my $V = $this->{$property};
			my $val;			
			if (UNIVERSAL::can($V, 'uri'))
				{ $val = Swignition::RDFModel::Value->new($V->uri, 1); }
			else
				{ $val = Swignition::RDFModel::Value->new($V); }
				
			$subject->addVal('http://buzzword.org.uk/rdf/vcardx#'.$property, $val);
		}
	}
	
	foreach my $m (@{ $this->{measures} })
	{
		$subject->addVal('http://buzzword.org.uk/rdf/measure-aux#hasMeasurement',
			Swignition::RDFModel::Value->new($m->uri, 1));
		$subject->addVal($m->dim_uri,
			Swignition::RDFModel::Value->new($m->qv_uri, 1))
			if (length $m->dim_uri);
	}
	
	return $subject;
	
} #/sub rdf_subject


sub metadata_ns
{
	my $this = shift;
	return {
		'v'         => { nsurl=>'http://www.w3.org/2006/vcard/ns#',title=>'vCard' },
		'vx'        => { nsurl=>'http://buzzword.org.uk/rdf/vcardx#',title=>'vCard Extensions' },
		'hcterms'   => { nsurl=>'http://purl.org/uF/hCard/terms/',title=>'hCard Terms'},
		'plink'     => { nsurl=>'http://buzzword.org.uk/rdf/personal-link-types#',title=>'Personal Link Types'},
		};
} #/sub namespace


sub parse
{
	my $page       = shift;
	my $rv         = { '_dom'=>shift };
	my $isInCal    = shift || 0;
	$rv->{isInCal} = $isInCal;

	my $pkg = __PACKAGE__ . ($isInCal?'::(cal)':'');
	if (defined $page->{uF}->{_Shortcuts}->{$pkg}->{ $rv->{_dom}->getAttribute('_xpath') })
		{ return $page->{uF}->{_Shortcuts}->{$pkg}->{ $rv->{_dom}->getAttribute('_xpath') }; }
	else
		{ $page->{uF}->{_Shortcuts}->{$pkg}->{ $rv->{_dom}->getAttribute('_xpath') } = $rv; }
	
	my $hcard_node = $rv->{'_dom'}->cloneNode(1);	
	
	my $id = $hcard_node->getAttribute('id');
	$rv->{_id} = $id if (length $id);
	$rv->{_page} = $page;
	$rv->{_xpath} = $rv->{_dom}->getAttribute('_xpath');
		
	Swignition::uF::data_patterns($page, $hcard_node, 6);
	
	my $properties = [
		['adr',         'm*',    {'embedded'=>'adr'}],
		['agent',       'MM*',   {'embedded'=>'hCard'}],
		['bday',        'd?'],
		['biota',       'm*',    {'embedded'=>'species', 'use-key'=>'species'}], #extension
		['birth',       'M?',    {'embedded'=>'hCard adr geo'}], #extension
		['caladruri',   'u*'],
		['caluri',      'MMu*',  {'embedded'=>'hCalendar'}],
		['category',    '*'],
		['class',       '?'],
		['dday',        'd?'],   #extension
		['death',       'M?',    {'embedded'=>'hCard adr geo'}], #extension
		['fn',          '1<'],
		['fburl',       'MMu*',  {'embedded'=>'hCalendar'}],
		['gender',      '?'],    #extension
		['geo',         'm*',    {'embedded'=>'geo'}],
		['kind',        '?'],    #extension
		['key',         'u*'],
		['lang',        '*'],    #extension
		['logo',        'u*'],
		['mailer',      '*'],
		['nickname',    '*'],
		['note',        '*'],
		['photo',       'u*'],
		['rev',         'd*'],
		['role',        '*'],
		['sort-string', '?'],
		['sound',       'u*'],
		['title',       '*'],
		['tz',          '?'],
		['uid',         'U?'],
		['url',         'u*']
	];
	push @{$properties}, (
		['cn',          '?'],
		['cutype',      '?'],
		['member',      '?'],
		['rsvp',        '?'],
		['delegated-from', 'Mu?',{'embedded'=>'hCard'}],
		['sent-by',     'Mu?',   {'embedded'=>'hCard'}],
	) if ($isInCal);
	push @{$properties}, (
		['member',      'Mu*',   {'embedded'=>'hCard'}], #extension
	) if (!$isInCal);

	simple_parse(
		$rv,
		$hcard_node,
		$properties,
		{
			'rel-me'     => '_has_relme',
			'rel-tag'    => 'category',
			'hmeasure'   => 'measures', #extension
			'no-destroy' => ['adr', 'geo'],
			'my-root'    => 'vcard'
		}
	);
	
	# Names
	my @names = searchClass('n', $hcard_node);
	@names = ($hcard_node) unless (@names);
	foreach my $name (@names)
	{
		my $n = Swignition::uF::hCard::Name::parse($page, $name);
		if ($n->{_n})
		{
			$n->{_hcard} = $rv;
			push @{ $rv->{n} }, $n;
		}
	}

	# Parse organisations
	my @orgs = searchClass('org', $hcard_node);
	my $is_root = 0;
	unless (@orgs)
	{
		@orgs = ($hcard_node); # Cope with missing 'org' wrapper.
		$is_root = 1;
	}
	foreach my $org (@orgs)
	{
		my $parsed_org = {
			'_id'    => $org->getAttribute('id'),
			'_page'  => $page,
			'_hcard' => $rv,
			'_dom'   => $org
		};
		simple_parse(
			$parsed_org,
			$hcard_node,
			[
				['organization-name',   '?'],
				['organization-unit',   '*'],
				['x-vat-number',        '?'],
				['x-charity-number',    '?'],
				['x-company-number',    '?'],
				['vat-number',          '?', {'use-key'=>'x-vat-number'}],
				['charity-number',      '?', {'use-key'=>'x-charity-number'}],
				['company-number',      '?', {'use-key'=>'x-company-number'}],
			],
			{ 'no-destroy' => ['adr', 'geo'] }
		);
		$parsed_org->{'organization-name'} = STRINGIFY($org, 'value')
			unless ($parsed_org->{'organization-name'} 
			||      $parsed_org->{'organization-unit'}
			||      $is_root);

		bless $parsed_org, 'Swignition::uF::hCard::org';
		push @{ $rv->{org} }, $parsed_org
			if ($parsed_org->{'organization-name'}
			||  $parsed_org->{'organization-unit'}->[0]);
	}
	
	# Telephone numbers and e-mail addresses
	my @comms = ('tel', 'email', 'impp', 'label');
	foreach my $comm (@comms)
	{
		my @nodes = searchClass($comm, $hcard_node);
		foreach my $n (@nodes)
		{
			my @accumValue;
			my @accumType;
			
			foreach my $v (searchClass('value', $n))
			{
				if ($comm eq 'label')
				{
					push @accumValue, STRINGIFY($v, undef, 1);
				}
				elsif ($v->localname eq 'a' || $v->localname eq 'area')
				{
					my $href = $v->getAttribute('href');

					push @accumType, 'fax' if ($href =~ /^fax\:/i);
					push @accumType, 'modem' if ($href =~ /^modem\:/i);

					$href =~ s/(^[a-z]+\:|\?.*$)//g unless ($comm eq 'impp');

					push @accumValue, $href;
				}
				elsif ($v)
				{
					push @accumValue, STRINGIFY($v, undef, 1);
				}
			}
			
			foreach my $v (searchClass('type', $n))
			{
				push @accumType, uc(STRINGIFY($v, undef, 1));
				$v->getParentNode->removeChild($v);
			}
			
			my $val = join '', @accumValue;
			if ($val !~ /[0-9a-z]/i)
			{
				if ($n->localname eq 'a' || $n->localname eq 'area')
				{
					$val = $n->getAttribute('href');
					$val =~ s/(^[a-z]+\:|\?.*$)//g;
				}
				else
				{
					$val = STRINGIFY($n, undef, 1);
				}
			}
			if ($comm eq 'tel')
			{
				$val = lc($val);
				$val =~ s/[^0-9x+-]//g;
			}
			
			my $C = {
					'value'  => $val,
					'type'   => \@accumType,
					'_id'    => $n->getAttribute('id'),
					'_page'  => $page,
					'_hcard' => $rv,
					'_dom'   => $n
				};
			bless $C, "Swignition::uF::hCard::$comm";
			push @{ $rv->{$comm} }, $C if (length $val);
		}
		
	}
	
	$rv->{fn} = $rv->{cn}
		unless ($rv->{fn});
	
	# Check to see if this contact card is for an individual contact or an org.
	# If for an org, disable n-optimisation.
	if ($rv->{kind})
	{
		$rv->{kind} =~ s/(^\s|\s+$)//g;
		$rv->{kind} = lc($rv->{kind});
	}
	elsif (length $rv->{fn})
	{
		$rv->{kind} = 'individual';
		
		ORGLOOP: foreach my $org (@{ $rv->{org} })
		{
			if ("".$org->{'organization-name'} eq $rv->{fn})
			{
				$rv->{kind} = 'org';
				last ORGLOOP;
			}
			foreach my $ou (@{ $org->{'organization-unit'} })
			{
				if ("$ou" eq $rv->{fn})
				{
					$rv->{kind} = 'group';
					last ORGLOOP;
				}
			}
		}
		
		if ($rv->{kind} eq 'individual')
		{
			ADRLOOP: foreach my $adr (@{ $rv->{adr} })
			{
				foreach my $part (qw(post-office-box extended-address
					street-address locality region postal-code country-name))
				{
					foreach my $line (@{ $adr->{$part} })
					{
						if ("$line" eq $rv->{fn})
						{
							$rv->{kind} = 'x-'.$part;
							last ADRLOOP;
						}
					}
				}
			}
		}
	}
	else
	{
		$rv->{kind} = 'individual';
	}

	# Perform n-optimisation.
	if ($rv->{kind} eq 'individual')
	{
		my $fnIsNick = 0;
		$fnIsNick if ((defined $rv->{'_fn_'}) && ($rv->{'_fn_'} =~ /\b(nickname)\b/));
		
		unless ($rv->{n} || $fnIsNick)
		{
			$rv->{fn} =~ s/(^\s|\s$)//g;
			$rv->{fn} =~ s/\s+/ /g;
			
			my @words = split / /, $rv->{fn};
			
			if (scalar @words == 1)
			{
				$rv->{nickname} = [ $words[0] ];
			}
			elsif (scalar @words)
			{
				if (($words[0] =~ /^.*\,$/ || $words[1] =~ /^.\.?$/) && !defined $words[2])
				{
					$words[0] =~ s/[\.\,]$//;
					$words[1] =~ s/[\.\,]$//;
					$rv->{n}->[0]->{'given-name'}->[0] = $words[1];
					$rv->{n}->[0]->{'family-name'}->[0] = $words[0];
					$rv->{n}->[0]->{_dom} = $rv->{'_fn_'};
					bless $rv->{n}->[0], 'Swignition::uF::hCard::Name';
				}
				elsif (!defined $words[2])
				{
					$rv->{n}->[0]->{'given-name'}->[0] = $words[0];
					$rv->{n}->[0]->{'family-name'}->[0] = $words[1];
					$rv->{n}->[0]->{_dom} = $rv->{'_fn_'};
					bless $rv->{n}->[0], 'Swignition::uF::hCard::Name';
				}
			}
		}
	}

	$rv->{source} = [];
	$rv->{IDENTIFIERS} = [];
	
	if (length $rv->{_id})
	{
		push @{$rv->{IDENTIFIERS}}, Swignition::uF::TDBURI($rv->{_page}->uri.'#'.$rv->{_id});
		push @{$rv->{source}}, $rv->{_page}->uri.'#'.$rv->{_id};
	}
	else
	{
		push @{$rv->{source}}, $rv->{_page}->uri;
	}
	if (lc($rv->{_dom}->tagName) eq 'body' || lc($rv->{_dom}->tagName) eq 'html')
	{
		push @{$rv->{IDENTIFIERS}}, Swignition::uF::TDBURI($rv->{_page}->uri);
	}
	if (length $rv->{uid})
	{
		push @{$rv->{IDENTIFIERS}}, UIDURI($rv, 'contact');
	}
	
	my $SGNM = Swignition::uF::getSGNM;
	
	if ($SGNM)
	{
		my @urls;
		push @urls, @{$rv->{url}}    if ($rv->{url});
		push @urls, @{$rv->{impp}}   if ($rv->{impp});
		push @urls, $rv->{uid}       if ($rv->{uid});
		
		foreach my $url (@urls)
		{
			my $sgn = $SGNM->graph_node_from_url("$url");
		
			if ($sgn =~ /^sgn:/)
			{
				foreach my $linktype (qw(profile rss atom foaf photo blog openid content addfriend))
				{
					my $u = $SGNM->graph_node_to_url($sgn, $linktype);
					$rv->{SGNURI}->{$linktype} = $u if ($u);
				}
				push @{$rv->{IDENTIFIERS}}, $sgn
			}
		}
	}

	bless $rv;
	Swignition::uF::error_list_process($rv, $page, 'http://microformats.org/wiki/hcard');
	return $rv;
	
} #/sub parse


sub rdf_validate
{
	my $this = shift;
	my $page =	shift;
	
	my $EARL = 'http://www.w3.org/WAI/ER/EARL/nmg-strawman#';
	my $XE   = 'http://buzzword.org.uk/rdf/xearl#';
	my $DC   = 'http://purl.org/dc/terms/';
	
	my @problems = $this->validate;
	foreach my $P (@problems)
	{
		my $p = Swignition::RDFModel::Subject->new(
			Swignition::GenericParser::Utils::BNodeURI(undef, 'MicroformatErrorReport'));		
		$p->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
			Swignition::RDFModel::Value->new($EARL.'Assertion', 1));
		$p->addVal($EARL.'assertedBy',
			Swignition::RDFModel::Value->new('http://buzzword.org.uk/swignition/#swignition', 1));
		$p->addVal($EARL.'mode',
			Swignition::RDFModel::Value->new($EARL.'automatic', 1));
		$p->addVal($XE.'complianceWith',
			Swignition::RDFModel::Value->new('http://microformats.org/wiki/hcard', 1));
		$p->addVal($EARL.'result',
			Swignition::RDFModel::Value->new($XE.($P->[2] eq 'E'?'error':'warning'), 1));
		$p->addVal($EARL.'subject',
			Swignition::RDFModel::Value->new($page->uri, 1));
		$p->addVal($XE.'itemOnPage',
			Swignition::RDFModel::Value->new($this->uri, 1));
		$p->addVal($DC.'abstract',
			Swignition::RDFModel::Value->new(strlang($P->[4], 'en')));
		$p->addVal($DC.'description',
			Swignition::RDFModel::Value->new(strlang($P->[5], 'en')));

		$page->mergeSubject($p);
	}
}


sub validate
{
	my $this = shift;
	my @rv;
	
#	unless (length $this->{fn})
#	{
#		push @rv, ['hCard', $this->uri, 'E', 1, "No formatted name (fn).", "The formatted name property is required."];
#	}

	if (defined $this->{n}->[1])
	{
		push @rv, ['hCard', $this->uri, 'E', 1, "Two 'n' properties.", "The hCard specification defines 'n' as a singular property."];
	}
	elsif ($this->{kind} eq 'individual' && !defined $this->{n}->[0])
	{
		push @rv, ['hCard', $this->uri, 'E', 1, "No 'n' property.", "hCards for individuals (as against hCards for organisations, groups or places) need an 'n' property, or need an 'fn' property that can be used to imply the 'n' property."];
	}
	
	foreach my $u (@{ $this->{url} })
	{
		my $U = URI->new($u);
		unless (length $U->scheme)
		{
			push @rv, ['hCard', $this->uri, 'E', 11, "URL property does not conform to IRI syntax.", "The URL property must be a valid address for a website or another type of IRI.", $u];
		}
	}
	
	if ($this->{uid})
	{
		my $U = URI->new($this->{uid});
		unless (length $U->scheme)
		{
			push @rv, ['hCard', $this->uri, 'W', 101, "UID property does not conform to IRI syntax.", "The UID property should be a valid address for a website or another type of IRI."];
		}
	}

	foreach my $prop (qw(photo logo sound))
	{
		my $n = ($prop eq 'photo')?2:(($prop eq 'logo')?3:4);
		foreach my $v (@{$this->{$prop}})
		{
			my $U = URI->new($v);
			unless (length $U->scheme)
			{
				push @rv, ['hCard', $this->uri, 'E', $n, uc($prop)." property does not conform to IRI syntax.", "This should be a resolvable URL or another type of IRI.", $v];
			}
		}
	}
	
	foreach my $t (@{$this->{tel}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{email}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{adr}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{org}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{geo}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{label}})
		{ push @rv, $t->validate; }
	foreach my $t (@{$this->{n}})
		{ push @rv, $t->validate; }
	
	return @rv;
}

1; #/package Swignition::uF::hCard



######################################################################
package Swignition::uF::hCard::tel;
######################################################################

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

#	if (length $this->{value})
#	{
#		my $v = $this->{value};
#		if ($v =~ /x/)
#		{
#			$v =~ s/x/;ext=/;
#		}
#		if ($v !~ /^\+/)
#		{
#			$v .= ';phone-context=unknown-context.invalid';
#		}
#		$v .= ';x-hcard-context=' . CGI::Util::escape($this->{_hcard}->uri)
#			if (defined $this->{_hcard});
#		push @rv, "tel:$v";
#	}
	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'ContactPhone')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub rdf_subject
{
	my $this    = shift;

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new('http://buzzword.org.uk/rdf/vcardx#Tel', 1));
	
	if (length $this->{value})
	{
		my $val = Swignition::RDFModel::Value->new($this->{'value'});
		$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#value', $val);
	}

	foreach my $V (@{ $this->{'type'} })
	{
		next unless (length $V);
		my $val = Swignition::RDFModel::Value->new($V);
		$subject->addVal('http://buzzword.org.uk/rdf/vcardx#usage', $val);
	}
	
	return $subject;
	
} #/sub rdf_subject

sub validate
{
	my $this = shift;
	my @rv;
	
	foreach my $t (@{$this->{type}})
	{
		if ($t !~ /^\s*(voice|home|msg|work|pref|fax|cell|video|pager|bbs|modem
			|car|isdn|pcs|x\-.+)\s*$/ix )
		{
			push @rv, ['hCard', $this->{'_hcard'}->uri, 'E', 21, "Invalid telephone number type.", "Valid telephone number types are: VOICE, home, msg, work, pref, fax, cell, video, pager, bbs, modem, car, isdn, pcs. If a non-standard type is required, it must be prefixed with 'X-'.", $t];		
		}
	}

	if ($this->{value} !~ /^\+/)
	{
		push @rv, ['hCard', $this->{'_hcard'}->uri, 'W', 121, "Local phone number.", "On the Web, international phone numbers (prefixed with a + sign) are more useful.", $this->{value}];		
	}
	
	return @rv;
	
} #/sub validate

1; #/package Swignition::uF::hCard::tel





######################################################################
package Swignition::uF::hCard::label;
######################################################################

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'ContactAddressLabel')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub rdf_subject
{
	my $this    = shift;

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new('http://buzzword.org.uk/rdf/vcardx#Label', 1));
	
	if (length $this->{value})
	{
		my $val = Swignition::RDFModel::Value->new($this->{'value'});
		$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#value', $val);
	}

	foreach my $V (@{ $this->{'type'} })
	{
		next unless (length $V);
		my $val = Swignition::RDFModel::Value->new($V);
		$subject->addVal('http://buzzword.org.uk/rdf/vcardx#usage', $val);
	}
	
	return $subject;
	
} #/sub rdf_subject

sub validate
{
	my $this = shift;
	my @rv;
	
	foreach my $t (@{$this->{type}})
	{
		if ($t !~ /^\s*(INTL|POSTAL|PARCEL|WORK|dom|home|pref|x\-.+)\s*$/ix )
		{
			push @rv, ['hCard', $this->{'_hcard'}->uri, 'E', 61, "Invalid label type.", "Valid label types are: INTL, POSTAL, PARCEL, WORK, dom, home and pref. If a non-standard type is required, it must be prefixed with 'X-'.", $t];		
		}
	}

	return @rv;
	
} #/sub validate

1; #/package Swignition::uF::hCard::label





######################################################################
package Swignition::uF::hCard::email;
######################################################################

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

#	if (length $this->{value})
#	{
#		my $v = $this->{value};
#		$v .= '?x-hcard-context=' . CGI::Util::escape($this->{_hcard}->uri)
#			if (defined $this->{_hcard});
#		push @rv, "mailto:$v";
#	}
	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'ContactEmail')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub rdf_subject
{
	my $this    = shift;

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new('http://buzzword.org.uk/rdf/vcardx#Email', 1));
	
	if (length $this->{'value'})
	{
		my $val = Swignition::RDFModel::Value->new($this->{'value'});
		$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#value', $val);
	}

	foreach my $V (@{ $this->{'type'} })
	{
		next unless (length $V);
		my $val = Swignition::RDFModel::Value->new($V);
		$subject->addVal('http://buzzword.org.uk/rdf/vcardx#usage', $val);
	}
	
	return $subject;
	
} #/sub rdf_subject

sub validate
{
	my $this = shift;
	my @rv;
	
	foreach my $t (@{$this->{type}})
	{
		if ($t !~ /^\s*(INTERNET|x400|pref|x\-.+)\s*$/ix)
		{
			push @rv, ['hCard', $this->{'_hcard'}->uri, 'E', 31, "Invalid e-mail type.", "Valid e-mail types are: internet, x400, pref. If a non-standard type is required, it must be prefixed with 'X-'.", $t];		
		}
	}

	if ($this->{value} !~ /\@/)
	{
		push @rv, ['hCard', $this->{'_hcard'}->uri, 'W', 131, "Non-RFC822 e-mail address.", "On the Web, standard RFC 822 e-mail addresses are more useful.", $this->{value}];		
	}
	
	return @rv;
	
} #/sub validate

1; #/package Swignition::uF::hCard::email


######################################################################
package Swignition::uF::hCard::impp;
######################################################################

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

#	if (length $this->{value})
#	{
#		my $v = $this->{value};
#		if (defined $this->{_hcard})
#		{
#			$v .= ($v =~ /\?/) ? '&' : '?';
#			$v .= 'x-hcard-context=' . CGI::Util::escape($this->{_hcard}->uri);
#		}
#		push @rv, $v;
#	}
	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'ContactChat')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub rdf_subject
{
	my $this    = shift;

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new('http://buzzword.org.uk/rdf/vcardx#Impp', 1));
	
	if (length $this->{value})
	{
		my $val = Swignition::RDFModel::Value->new($this->{'value'});
		$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#value', $val);
	}

	foreach my $V (@{ $this->{'type'} })
	{
		next unless (length $V);
		my $val = Swignition::RDFModel::Value->new($V);
		$subject->addVal('http://buzzword.org.uk/rdf/vcardx#usage', $val);
	}
	
	return $subject;
	
} #/sub rdf_subject

sub validate
{
	my $this = shift;
	my @rv;
	
	return @rv;
	
} #/sub validate

1; #/package Swignition::uF::hCard::impp


######################################################################
package Swignition::uF::hCard::org;
######################################################################

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

#	if (length $this->{'organization-name'})
#	{
#		my $v = 'o='.CGI::Util::escape($this->{'organization-name'});
#		foreach my $ou ( @{$this->{'organization-unit'}} )
#		{
#			$v .= ',ou='.CGI::Util::escape($ou);
#		}
#		push @rv, "ldap:///$v";
#	}
	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'Org')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub rdf_subject
{
	my $this    = shift;

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new('http://www.w3.org/2006/vcard/ns#'.'Organization', 1));
	
	# Patterns for simple data.
	my @_SINGLES  = qw(organization-name x-charity-number x-vat-number
		x-company-number);
	my @_MULTIS   = qw(organization-unit);
	
	# Properties which exist as singles.
	foreach my $property (@_SINGLES)
	{
		my $ns = 'http://buzzword.org.uk/rdf/vcardx#';
		$ns = 'http://www.w3.org/2006/vcard/ns#' if ($property eq 'organization-name');
		
		next unless (length $this->{$property});
		my $val = Swignition::RDFModel::Value->new($this->{$property});
		$subject->addVal($ns.$property, $val);
	}

	# Properties which exist as multiple values.
	foreach my $property (@_MULTIS)
	{
		foreach my $V (@{ $this->{$property} })
		{
			my $ns = 'http://buzzword.org.uk/rdf/vcardx#';
			next unless (length $V);
			my $val = Swignition::RDFModel::Value->new($V);
			$subject->addVal($ns.$property, $val);
		}
	}
	
	return $subject;
	
} #/sub rdf_subject


sub validate
{
	my $this = shift;
	my @rv;
	
	if ($this->{implied})
	{
		push @rv, ['hCard', $this->{'_hcard'}->uri, 'E', 51, "Organisation subtypes found without an 'org' wrapper.", "The 'organization-name' and 'organization-unit' properties are only valid within an 'org'.", $this->{'organization-name'}];		
	}
	
	return @rv;
	
} #/sub validate


1; #/package Swignition::uF::hCard::org




######################################################################
package Swignition::uF::hCard::Name;
######################################################################

use Swignition::GenericParser::Utils;
use Swignition::uF;

sub uri
{
	my $this = shift;
	my $all  = shift;
	my @rv;

	if (length $this->{_id})
	{
		push @rv, Swignition::uF::TDBURI($this->{_page}->uri.'#'.$this->{_id});
	}
	push @rv, Swignition::GenericParser::Utils::BNodeURI($this->{_dom}, 'ContactName')
		unless (@rv);
	
	return $rv[0] unless (defined $all);
	while ($all) { shift @rv; $all--; }
	return @rv;
} #/sub uri

sub parse
{
	my $page = shift;
	my $dom  = shift;
	my $rv   = {};
	
	simple_parse(
		$rv,
		$dom,
		[
			['additional-name',  '*'],
			['family-name',      '*'],
			['given-name',       '*'],
			['honorific-prefix', '*'],
			['honorific-suffix', '*'],
			['initial',          '*'], # extension
		],
		{
			'no-destroy' => ['adr', 'geo']
		}
	);

	$rv->{_n}    = 1
		if ($rv->{'additional-name'}
		||  $rv->{'family-name'}
		||  $rv->{'given-name'}
		||  $rv->{'honorific-prefix'}
		||  $rv->{'honorific-suffix'}
		||  $rv->{'initials'});
	$rv->{_id}   = $dom->getAttribute('id') if (length $dom->getAttribute('id'));
	$rv->{_dom}  = $dom;
	$rv->{_page} = $page;
	
	bless $rv;
}

sub rdf_subject
{
	my $this    = shift;
	my $ns      = 'http://www.w3.org/2006/vcard/ns#';

	# Create RDF subject
	my $subject = Swignition::RDFModel::Subject->new($this->uri);
	$subject->addObject($this);
	$subject->setDom($this->{_dom});
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
		Swignition::RDFModel::Value->new($ns.'Name', 1));
	
	# Patterns for simple data.
	my @_MULTIS   = qw(family-name given-name additional-name honorific-prefix
	                   honorific-suffix initial);
	
	# Properties which exist as multiple values.
	foreach my $property (@_MULTIS)
	{
		foreach my $V (@{ $this->{$property} })
		{
			next unless (length $V);
			my $val = Swignition::RDFModel::Value->new($V);
			$subject->addVal($ns.$property, $val);
		}
	}
	
	my $value = '';
	foreach my $prop (qw(family-name given-name additional-name honorific-prefix
	                   honorific-suffix))
	{
		my $v = join 'XCOMMAX', @{ $this->{$prop} };
		$v =~ s/\\/\\\\/g;
		$v =~ s/\,/\\\,/g;
		$v =~ s/\;/\\\;/g;
		$v =~ s/XCOMMAX/\,/g;
		
		$value .= $v.';';
	}
	$value =~ s/\;+$//g;
	
	$subject->addVal('http://www.w3.org/1999/02/22-rdf-syntax-ns#value',
		Swignition::RDFModel::Value->new($value))
		if (length $value);
	
	$value = '';
	foreach my $prop (qw(honorific-prefix given-name additional-name family-name
	                   honorific-suffix))
	{
		my $v = join ' ', @{ $this->{$prop} };
		$value .= $v.' ' if (length $v);
	}
	$value =~ s/\s+$//g;
	
	$subject->addVal('http://www.w3.org/2000/01/rdf-schema#label',
		Swignition::RDFModel::Value->new($value))
		if (length $value);
	
	return $subject;
	
} #/sub rdf_subject


sub validate
{
	my $this = shift;
	my @rv;
	
	return @rv;
	
} #/sub validate



1; #/package Swignition::uF::hCard::Name
